package com.shell.guard.controller;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.google.code.kaptcha.Producer;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.shell.bee.base.exception.UnCheckMsgException;
import com.shell.bee.base.utils.SpringContextUtil;
import com.shell.bee.base.utils.StringUtil;
import com.shell.bee.base.utils.SystemUtil;
import com.shell.bee.entity.auth.AuthUser;
import com.shell.bee.entity.mvc.RetMsg;
import com.shell.constant.ind.PubConstants;
import com.shell.feign.AuthFeign;
import com.shell.feign.PubAccessFeign;
import com.shell.feign.PubParameterFeign;
import com.shell.feign.PubSecurityRuleFeign;
import com.shell.governor.access.po.PubAccess;
import com.shell.governor.modules.vo.ParameterVo;
import com.shell.governor.security.user.vo.RuleCheckMsg;
import com.shell.guard.security.CustomAuthorizationTokenServices;
import com.shell.guard.security.filter.CustomLogoutHandler;
import com.shell.guard.service.LoginInfoService;
import com.shell.guard.utils.HttpUtil;
import com.shell.guard.utils.QRCodeLoginUtil;
import com.shell.guard.vo.AuthVo;
import com.shell.guard.vo.LoginUuid;

import io.goeasy.GoEasy;
import io.goeasy.publish.GoEasyError;
import io.goeasy.publish.PublishListener;

@DependsOn({ "authorizationServerTokenServices" })
@RestController
public class AuthController {
	private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
	@Autowired
	private LoadBalancerClient loadBalancerClient;
	
	@Autowired
	private Producer captchaProducer;
	@Autowired
	private CustomLogoutHandler customLogoutHandler;

	@Value("${guard.broadcast.notify.serviceApp}")
	private String serviceApp;

	RestTemplate restTemplate = new RestTemplate();
	@Autowired
	private StringRedisTemplate redisTemplate;
	@Autowired
	private LoginInfoService loginInfoService;
	@Autowired
	private CustomAuthorizationTokenServices authorizationServerTokenServices;
	@Autowired
	private AuthFeign authFeign;
	@Autowired
	private PubSecurityRuleFeign pubSecurityRuleFeign;
	@Autowired
	private PubParameterFeign parameterFeign;
	@Autowired
	private PubAccessFeign pubAccessFeign;
	private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();

	@RequestMapping(value = { "/genLoginQrcode/{uuid}" }, method = {
			org.springframework.web.bind.annotation.RequestMethod.GET })
	public void genLoginQrcode(@PathVariable String uuid, HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		try {
			Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();

			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

			hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
			hints.put(EncodeHintType.MARGIN, Integer.valueOf(1));
			String appDownloadUrl = SpringContextUtil.getProperty("chainbill.app.download");
			BitMatrix bitMatrix = new MultiFormatWriter().encode(appDownloadUrl + "?uuid-" + uuid,
					BarcodeFormat.QR_CODE, 300, 300, hints);
			OutputStream out = response.getOutputStream();
			MatrixToImageWriter.writeToStream(bitMatrix, "png", out);
			out.flush();
			out.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@RequestMapping(value = { "/getLoginUUID" }, method = { org.springframework.web.bind.annotation.RequestMethod.GET })
	public String getLoginUUID(HttpServletRequest request, HttpServletResponse response) {
		String uuid = SystemUtil.getUUID();
		LoginUuid loginUuid = new LoginUuid(uuid, false);
		QRCodeLoginUtil.set(loginUuid);
		return uuid;
	}

	@RequestMapping(value = { "/getUserInfoByScanCode" }, method = {
			org.springframework.web.bind.annotation.RequestMethod.POST })
	public void getUserInfoByScanCode(String userName, final String uuid, HttpServletRequest request,
			HttpServletResponse response) {
		if (QRCodeLoginUtil.get(uuid) == null) {
			throw new UnCheckMsgException("请扫描通宝系统的登录二维码!");
		}
		AuthUser user = this.authFeign.getUserByPrincipal(userName);
		GoEasy goEasy = new GoEasy("http://rest-hangzhou.goeasy.io", "BC-196e3b57423b432f9385a6c461cd5d72");
		JSONObject object = new JSONObject();
		try {
			object.put("username", user.getLoginName());
			goEasy.publish(uuid, object.toString(), new PublishListener() {
				public void onSuccess() {
					AuthController.logger.info("二维码登录 " + uuid + "频道消息发送成功");
				}
				public void onFailed(GoEasyError error) {
					AuthController.logger
							.error("消息发布失败, 错误编码" + error.getCode() + "错误信息：" + error.getContent());
				}
			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@RequestMapping(value = { "/login" }, method = { RequestMethod.POST })
	public RetMsg login(AuthVo authVo, HttpServletRequest request, HttpServletResponse response)
			throws IOException, ServletException {
		Map<String, Object> map = new HashMap<String, Object>();
		RetMsg ret = new RetMsg();
		if (StringUtil.empty(authVo.getTerminalCode())) {
			authVo.setTerminalCode(PubConstants.TerminalCode.TRML_PC);
		}
		if (!authVo.isQrcodeLogin()) {
			if (!checkAuthVo(authVo, ret)) {
				return ret;
			}
			if ((!authVo.getTerminalCode().equals(PubConstants.TerminalCode.TRML_MOBILE)) && (!authVo.isMsgLogin())
					&& (!checkCode(request, authVo.getKaptchaReceived(), ret))) {
				return ret;
			}
			if ((!"mobile".equals(authVo.getClient())) && (!authVo.isMsgLogin())
					&& (!checkCode(request, authVo.getKaptchaReceived(), ret))) {
				return ret;
			}
		} else {
			String uuid = authVo.getUuid();
			LoginUuid loginUuid = QRCodeLoginUtil.get(uuid);
			if (loginUuid == null) {
				throw new UnCheckMsgException("请扫描链单系统的登录二维码！");
			}
			if (loginUuid.isUsed()) {
				throw new UnCheckMsgException("二维码已被使用");
			}
			loginUuid.setUsed(true);
			QRCodeLoginUtil.set(loginUuid);
		}
		MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
		formData.add("username", authVo.getUsername());
		formData.add("password", authVo.getPassword());
		formData.add("client", authVo.getClient());
		formData.add("grant_type", authVo.getGrant_type());
		formData.add("msgVerifyCode", authVo.getMsgVerifyCode());
		formData.add("isMsgLogin", authVo.isMsgLogin() + "");
		formData.add("entId", authVo.getEntId());
		formData.add("terminalCode", authVo.getTerminalCode());
		formData.add("isQrcodeLogin", authVo.isQrcodeLogin() + "");

		String client = authVo.getClient();
		String clientSecret = this.loginInfoService.queryClientSecret(client);
		ServiceInstance serviceInstance = this.loadBalancerClient.choose("guard");
		if (serviceInstance == null) {
			throw new RuntimeException("Failed to choose an auth instance.");
		}
		map = postForMap(serviceInstance.getUri().toString() + SpringContextUtil.getProperty("server.context-path", "")
				+ "/oauth/token", formData, setHeader(client, clientSecret));
		if ((map != null) && (map.containsKey("type")) && (((String) map.get("type")).equals("error"))) {
			return new RetMsg("error", (String) map.get("msg"));
		}
		String entId = authVo.getEntId();
		if (StringUtil.empty(entId)) {
			entId = (String) map.get("X-AOHO-ENT");
		}
		if (!checkAccessAuthority(authVo.getUsername(), request, ret)) {
			return ret;
		}
		loginSuccess(authVo.getUsername(), authVo.getAppCode(), authVo.getTerminalCode(), entId, map, response, ret);
		if ((PubConstants.TerminalCode.TRML_PC.equals(authVo.getTerminalCode())) && (!authVo.isQrcodeLogin())) {
			AuthUser user = this.authFeign.getUserByPrincipal(authVo.getUsername());
			RuleCheckMsg ruleCheckMsg = this.pubSecurityRuleFeign.checkSecurityRule(user.getUserId(),
					user.getPassword(), "login", true);
			if (!ruleCheckMsg.getIsOk()) {
				return new RetMsg("info", false,
						"登录成功！但登录密码不符合要求，请尽快修改！详情：" + ruleCheckMsg.getCheckMsg());
			}
		}
		logger.info("=============" + authVo.getUsername() + ",登录成功===================");
		return ret;
	}

	private boolean checkAccessAuthority(String username, HttpServletRequest request, RetMsg ret) {
		String accessType = request.getHeader("accessType");
		if ((StringUtil.emptyAndNull(accessType)) || ("ALL".equals(accessType))) {
			return true;
		}
		ParameterVo parameter = this.parameterFeign.getCommonParameter("NETWORK_ACCESS_CONTROL");
		if ((parameter == null) || (StringUtil.emptyAndNull(parameter.getParmValue()))) {
			return true;
		}
		if ("1".equals(parameter.getParmValue())) {
			AuthUser authUser = this.authFeign.getUserByPrincipal(username);
			String entId = authUser.getEnt().getOrganId();
			ServiceInstance serviceInstance = this.loadBalancerClient.choose("customer");
			if (serviceInstance == null) {
				throw new RuntimeException("Failed to choose customer instance.");
			}
			MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
			map.add("entId", entId);
			map.add("grpId", "");
			String tagCodes = (String) this.restTemplate.postForObject(
					serviceInstance.getUri().toString() + "/customer/customer/getTagCodesById", map, String.class,
					new Object[0]);
			if (StringUtil.notEmpty(tagCodes)) {
				List<PubAccess> accessList = this.pubAccessFeign.selectByTagCodes(tagCodes);
				boolean canAccess = false;
				for (PubAccess access : accessList) {
					if (("ALL".equals(access.getAccessType()))
							|| (accessType.equalsIgnoreCase(access.getAccessType()))) {
						canAccess = true;
						break;
					}
				}
				if (!canAccess) {
					ret.setType("warning");
					ret.setMsg("无网络访问权限");
					return false;
				}
			}
		}
		return true;
	}

	private void loginSuccess(String userName, String appCode, String terminalCode, String entId,
			Map<String, Object> resultMap, HttpServletResponse response, RetMsg ret) {
		if (StringUtil.empty(entId)) {
			entId = "";
		}
		String userId = (String) resultMap.get("X-AOHO-UserId");
		notifyAfterLogged(userId, entId, appCode, terminalCode);

		String access_token = (String) resultMap.get("access_token");
		String token = "Bearer" + access_token;
		response.setHeader("Authorization", token);

		resultMap.put("sso", Boolean.valueOf(true));
		ret.setType("success");
		ret.setShow(true);
		ret.setMsg("登录成功");
		ret.setBean(resultMap);

		Cookie tokenCookie = new Cookie("access_token", token);
		tokenCookie.setMaxAge(-1);
		tokenCookie.setPath("/");
		response.addCookie(tokenCookie);

		String key = access_token + "_time";
		this.redisTemplate.opsForValue().set(key, "alive", 3600L, TimeUnit.SECONDS);
	}

	private boolean checkAuthVo(AuthVo authVo, RetMsg ret) {
		if (StringUtil.emptyAndNull(authVo.getUsername())) {
			ret.setType("warning");
			ret.setMsg("请输入手机号/登录名！");
			return false;
		}
		if ((!authVo.isMsgLogin()) && (StringUtil.emptyAndNull(authVo.getPassword()))) {
			ret.setType("warning");
			ret.setMsg("请输入密码！");
			return false;
		}
		if ((authVo.isMsgLogin()) && (StringUtil.emptyAndNull(authVo.getMsgVerifyCode()))) {
			ret.setType("warning");
			ret.setMsg("请输入验证码！");
			return false;
		}
		return true;
	}

	private boolean checkCode(HttpServletRequest request, String kaptchaReceived, RetMsg ret) {
		String code = HttpUtil.getCookieValue(request, "KAPTCHA_SESSION_KEY");
		boolean checkSuccess = true;
		if (StringUtil.emptyAndNull(kaptchaReceived)) {
			checkSuccess = false;
			ret.setType("warning");
			ret.setMsg("请输入验证码！");
		} else if (code == null) {
			checkSuccess = false;
			ret.setType("warning");
			ret.setMsg("验证码失效！");
		} else if (!code.equals(kaptchaReceived)) {
			checkSuccess = false;
			ret.setType("error");
			ret.setMsg("验证码输入错误！");
		}
		return checkSuccess;
	}

	@RequestMapping({ "/auth/getVerifycode" })
	public void getVerifyCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
		try {
			logger.debug("------------begin get verify code pic ----------");

			response.setDateHeader("Expires", 0L);

			response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");

			response.addHeader("Cache-Control", "post-check=0, pre-check=0");

			response.setHeader("Pragma", "no-cache");

			response.setContentType("image/jpeg");

			String capText = this.captchaProducer.createText();

			Cookie imageCode = new Cookie("KAPTCHA_SESSION_KEY", capText);

			imageCode.setPath("/");
			response.addCookie(imageCode);

			BufferedImage bi = this.captchaProducer.createImage(capText);
			ServletOutputStream out = response.getOutputStream();

			ImageIO.write(bi, "jpg", out);
			try {
				out.flush();
			} finally {
				out.close();
			}
			logger.debug("------------end get verify code pic ----------");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@RequestMapping(value = { "/auth/change-ent" }, method = {
			org.springframework.web.bind.annotation.RequestMethod.POST })
	public RetMsg changeEnt(AuthVo authVo, HttpServletRequest request, HttpServletResponse response)
			throws IOException, ServletException {
		RetMsg ret = new RetMsg();
		Cookie[] cookies = request.getCookies();
		String token = null;
		if (null != cookies) {
			for (Cookie cookie : cookies) {
				if (cookie.getName().equals("access_token")) {
					token = cookie.getValue();
					break;
				}
			}
		}
		token = token.substring(6);
		OAuth2AccessToken realToken = this.authorizationServerTokenServices.readAccessToken(token);
		OAuth2Authentication authentication = this.authorizationServerTokenServices
				.loadAuthentication(realToken.getValue());
		UserDetails userDetails = (UserDetails) authentication.getPrincipal();

		MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
		if (StringUtil.emptyAndNull(userDetails.getPassword())) {
			ret.setMsg("当前不是使用密码登录，请重新登录再切换企业！");
			ret.setType("warning");
			return ret;
		}
		if (StringUtil.empty(authVo.getTerminalCode())) {
			authVo.setTerminalCode(PubConstants.TerminalCode.TRML_PC);
		}
		authVo.setUsername(userDetails.getUsername());
		authVo.setPassword(userDetails.getPassword());
		formData.add("entId", authVo.getEntId());
		formData.add("username", authVo.getUsername());
		formData.add("password", authVo.getPassword());
		formData.add("client", authVo.getClient());
		formData.add("grant_type", authVo.getGrant_type());
		formData.add("terminalCode", authVo.getTerminalCode());

		String client = authVo.getClient();
		String clientSecret = this.loginInfoService.queryClientSecret(client);
		ServiceInstance serviceInstance = this.loadBalancerClient
				.choose(SpringContextUtil.getProperty("spring.application.name", "guard"));
		if (serviceInstance == null) {
			throw new RuntimeException("Failed to choose an auth instance.");
		}
		Map<String, Object> map = new HashMap<String, Object>();
		map = postForMap(serviceInstance.getUri().toString() + SpringContextUtil.getProperty("server.context-path", "")
				+ "/oauth/token", formData, setHeader(client, clientSecret));
		if ((map != null) && (map.containsKey("type")) && (((String) map.get("type")).equals("error"))) {
			throw new UnCheckMsgException((String) map.get("msg"));
		}
		loginSuccess(authVo.getUsername(), authVo.getAppCode(), authVo.getTerminalCode(), authVo.getEntId(), map,
				response, ret);
		ret.setMsg("企业切换成功");
		return ret;
	}

	@RequestMapping(value = { "/auth/check_token" }, method = {
			org.springframework.web.bind.annotation.RequestMethod.POST })
	@ResponseBody
	public ResponseEntity<Map<String, Object>> checkToken(@RequestParam("token") String token)
			throws IOException, ServletException {
		HttpHeaders headers = new HttpHeaders();
		headers.set("Cache-Control", "no-store");
		headers.set("Pragma", "no-cache");
		Map<String, Object> result = new HashMap<String, Object>();
		ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<Map<String, Object>>(result, headers,
				HttpStatus.OK);
		if (isJwtBearerToken(token)) {
			token = token.substring(6);
			OAuth2AccessToken realToken = this.authorizationServerTokenServices.readAccessToken(token);
			if (realToken == null) {
				logger.error("Token was not recognised");
				result.put("success", Boolean.valueOf(false));
				result.put("isForcedLogout", Boolean.valueOf(
						!StringUtil.emptyAndNull((String) this.redisTemplate.opsForValue().get(token + "_time"))));
				return responseEntity;
			}
			OAuth2Authentication authentication = this.authorizationServerTokenServices
					.loadAuthentication(realToken.getValue());

			Map<String, ?> response = this.accessTokenConverter.convertAccessToken(realToken, authentication);

			result.put("response", response);

			String key = token + "_time";
			String valid = (String) this.redisTemplate.opsForValue().get(key);
			if (valid == null) {
				logger.error("Token has expired");
				result.put("success", Boolean.valueOf(false));
				return responseEntity;
			}
			this.redisTemplate.opsForValue().set(key, "alive", 3600L, TimeUnit.SECONDS);
		}
		result.put("success", Boolean.valueOf(true));
		return responseEntity;
	}

	private boolean isJwtBearerToken(String token) {
		return (StringUtils.countMatches(token, ".") == 2)
				&& ((token.startsWith("Bearer")) || (token.startsWith("bearer")));
	}

	private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
		if (headers.getContentType() == null) {
			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		}
		Map<String, Object> result = this.restTemplate.exchange(path, HttpMethod.POST,
				new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class, new Object[0]).getBody();
		return result;
	}

	public HttpHeaders setHeader(String clientId, String clientSecret) throws IOException, ServletException {
		HttpHeaders headers = new HttpHeaders();
		String authHead = getAuthorizationHeader(clientId, clientSecret);
		headers.set("Authorization", authHead);
		headers.set("X-Requested-With", "XMLHttpRequest");
		return headers;
	}

	private String getAuthorizationHeader(String clientId, String clientSecret) {
		String creds = String.format("%s:%s", new Object[] { clientId, clientSecret });
		try {
			return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
		} catch (UnsupportedEncodingException e) {
			throw new IllegalStateException("Could not convert String");
		}
	}

	private void notifyAfterLogged(String userId, String entId, String appCode, String terminalCode) {
		if (StringUtil.empty(this.serviceApp)) {
			logger.info("没有订阅登录后通知的服务");
			return;
		}
		String[] serviceList = this.serviceApp.split(",");

		MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
		map.add("userId", userId);
		map.add("entId", entId);
		map.add("appCode", appCode);
		map.add("terminalCode", terminalCode);

		ServiceInstance serviceInstance = null;
		for (String serviceName : serviceList) {
			serviceInstance = this.loadBalancerClient.choose(serviceName);
			if (serviceInstance == null) {
				logger.error("获取消息时候，找不到对应的服务：" + serviceName);
			} else {
				logger.info("获取登录后通知服务" + serviceName + "--------->" + serviceInstance.getUri().toString()
						+ "/" + serviceName + "/afterLogged");
				this.restTemplate.postForObject(
						serviceInstance.getUri().toString() + "/" + serviceName + "/afterLogged", map, Map.class,
						new Object[0]);
			}
		}
	}

	@RequestMapping({ "/logout" })
	public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		this.customLogoutHandler.logout(request, response, null);
	}
}